﻿// -----------------------------------------------------------------------
// <copyright file="PushService.cs" company="Zen Design Corp">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Zen.BlackBerry.Push
{
	using System;
	using System.Collections.Generic;
	using System.IO;
	using System.Linq;
	using System.Text;
	using System.Threading.Tasks;
	using Microsoft.Practices.Unity;
	using Zen.BlackBerry.Common;
	using Zen.BlackBerry.Configuration;
	using Zen.BlackBerry.Pap;
	using Zen.BlackBerry.Pap.Control;
	using Zen.BlackBerry.Pap.Parser;
	using Zen.BlackBerry.Push.AppManagement;
	using Zen.BlackBerry.Push.Count;
	using Zen.BlackBerry.Push.Request;
	using Zen.BlackBerry.Push.Stats;
	using Zen.BlackBerry.Subscription;

	/// <summary>
	/// Implementation of the <see cref="IPushService"/> interface.
	/// </summary>
	public class PushService : IPushService
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="PushService"/> class.
		/// </summary>
		public PushService()
		{
		}

		/// <summary>
		/// Gets or sets the push properties.
		/// </summary>
		/// <value>The push properties.</value>
		[Dependency]
		public IPushProperties PushProperties
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the pap service.
		/// </summary>
		/// <value>The pap service.</value>
		[Dependency]
		public IPapService PapService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the application service.
		/// </summary>
		/// <value>The application service.</value>
		[Dependency]
		public IPushApplicationService ApplicationService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the count service.
		/// </summary>
		/// <value>The count service.</value>
		[Dependency]
		public IPushCountService CountService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the id generator.
		/// </summary>
		/// <value>The id generator.</value>
		[Dependency]
		public IIdGenerator IdGenerator
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the request service.
		/// </summary>
		/// <value>The request service.</value>
		[Dependency]
		public IPushRequestService RequestService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the request detail service.
		/// </summary>
		/// <value>The request detail service.</value>
		[Dependency]
		public IPushRequestDetailService RequestDetailService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the stats service.
		/// </summary>
		/// <value>The stats service.</value>
		[Dependency]
		public IPushStatsService StatsService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the subscription service.
		/// </summary>
		/// <value>The subscription service.</value>
		[Dependency]
		public ISubscriptionService SubscriptionService
		{
			get;
			set;
		}

		/// <summary>
		/// Validates the push.
		/// </summary>
		/// <param name="pushParameters">The push parameters.</param>
		/// <returns></returns>
		protected PushApplication ValidatePush(PushParameters pushParameters)
		{
			if (pushParameters == null)
			{
				throw new ArgumentNullException("pushParameters");
			}
			if (string.IsNullOrWhiteSpace(pushParameters.ApplicationId))
			{
				throw new ArgumentNullException(
					"pushParameters.ApplicationId", "Application id is null.");
			}

			PushApplication application = ValidateApplicationForPush(pushParameters.ApplicationId);
			pushParameters.Validate(application.Type);

			return application;
		}

		/// <summary>
		/// Validates the application for push.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <returns></returns>
		protected PushApplication ValidateApplicationForPush(string applicationId)
		{
			return ApplicationService.FindByIdAndValidate(applicationId);
		}

		private string GetSubscriberId(PushApplication application, string pushId, string address)
		{
			if (address.Equals("push_all"))
			{
				return string.Empty;
			}

			if (application.BypassSubscriptions)
			{
				return address;
			}

			if (application.IsStorePushRequests)
			{
				PushRequestDetail detail = RequestDetailService.FindByIdAndAddress(pushId, address);
				return ((detail != null) ? detail.SubscriberId : "unknown");
			}

			string subscriberId = "unknown";
			var subscriber = SubscriptionService
				.FindByAppIdAndAddress(application.RimApplicationId, address)
				.FirstOrDefault((item) => item.Status == SubscriberStatus.Active);
			if (subscriber != null)
			{
				subscriberId = subscriber.SubscriberId;
			}

			return subscriberId;
		}

		private PushMessageControl CreatePushMessageControl(
			bool usingPublicPush,
			string pushId,
			PushParameters pushParameters,
			PushApplication pushApplication,
			IEnumerable<string> activeAddresses,
			int totalActiveAddress)
		{
			PushMessageControl pushMessageControl = new PushMessageControl(usingPublicPush);
			pushMessageControl.PushId = pushId;
			pushMessageControl.UserName = pushParameters.ApplicationId;

			DateTime? deliverBeforeTimestamp = pushParameters.DeliverBeforeDate;
			if (deliverBeforeTimestamp == null)
			{
				deliverBeforeTimestamp =
					DateTime.UtcNow.AddDays(13) +
					pushApplication.DefaultPushLiveTime;
			}
			pushMessageControl.DeliverBefore = deliverBeforeTimestamp.Value;

			pushMessageControl.Addresses.AddRange(activeAddresses);

			bool? isApplicationReliable = pushParameters.IsApplicationReliable;
			if (isApplicationReliable == null)
			{
				isApplicationReliable = pushApplication.IsApplicationReliable;
			}

			if (isApplicationReliable.Value)
			{
				pushMessageControl.QualityOfService = DeliveryMethod.Confirmed;
			}
			else
			{
				pushMessageControl.QualityOfService = DeliveryMethod.Unconfirmed;
			}

			pushMessageControl.PushProperties = PushProperties;
			if (usingPublicPush)
			{
				pushMessageControl.PpgNotifyRequestTo = pushApplication.PublicNotifyUrl;
			}
			else
			{
				pushMessageControl.PpgNotifyRequestTo = pushApplication.EnterpriseNotifyUrl;
			}

			return pushMessageControl;
		}

		private string ValidatePushId(string pushId, bool isStoringPushRequests)
		{
			string currentPushId = pushId;
			string validatedPushId = pushId;

			try
			{
				if (string.IsNullOrWhiteSpace(currentPushId))
				{
					int index = 0;
					bool generatedValidId = false;
					do
					{
						currentPushId = IdGenerator.GenerateId();
						++index;

						if (!isStoringPushRequests)
						{
							generatedValidId = true;
						}
						else
						{
							PushRequest request = RequestService.FindById(currentPushId);
							if (request != null)
							{
								continue;
							}

							generatedValidId = true;
						}

						validatedPushId = currentPushId;
						break;
					}
					while (index < PushProperties.RegeneratePushIdMaxAttempts);

					if (!generatedValidId)
					{
						throw new InvalidOperationException("Exceeded maximum number of attempts to generate unique push id.");
					}
				}
				else if (isStoringPushRequests)
				{
					PushRequest request = RequestService.FindById(currentPushId);
					if (request != null)
					{
						throw new InvalidOperationException("Push request issued with push id that already exists.");
					}
				}
			}
			catch (Exception exception)
			{
				throw new InvalidOperationException("Unable to lookup push id " + currentPushId + ".", exception);
			}

			return validatedPushId;
		}

		private string GetNotifyUrl(PushApplication pushApplication, int totalActiveAddresses, string notifyUrl)
		{
			if (!string.IsNullOrEmpty(notifyUrl))
			{
				StringBuilder sb = new StringBuilder(notifyUrl);
				char seperator = '?';
				if (notifyUrl.Contains('?'))
				{
					seperator = '&';
				}

				sb.AppendFormat("{0}appid={1}", seperator, pushApplication.RimApplicationId);
				if (pushApplication.IsTrackingLastNotification)
				{
					sb.AppendFormat("&address_count={0}", totalActiveAddresses);
				}
				return sb.ToString();
			}
			return null;
		}

		private int CalculateContentLength(Content content)
		{
			using (MemoryStream stream = new MemoryStream())
			{
				using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8))
				{
					content.WriteTo(writer);
					writer.Flush();
					return (int)stream.Length;
				}
			}
		}

		private PushResult CreatePushResult(
			string pushId,
			PushResponse bisPushResponse,
			PushResponse besPushResponse)
		{
			if ((bisPushResponse != null) && (besPushResponse != null))
			{
				if (bisPushResponse.Code.Code.StartsWith("1") &&
					besPushResponse.Code.Code.StartsWith("1"))
				{
					return new PushResult(pushId, StatusCode.Accepted);
				}
				if (bisPushResponse.Code == StatusCode.DuplicatePushId ||
					besPushResponse.Code == StatusCode.DuplicatePushId)
				{
					return new PushResult(pushId, StatusCode.DuplicatePushId);
				}
				if (bisPushResponse.Code.Code.StartsWith("1") &&
					!besPushResponse.Code.Code.StartsWith("1"))
				{
					return new PushResult(pushId, StatusCode.Accepted);
				}
				if (!bisPushResponse.Code.Code.StartsWith("1") &&
					besPushResponse.Code.Code.StartsWith("1"))
				{
					return new PushResult(pushId, StatusCode.Accepted);
				}
				return new PushResult(pushId, bisPushResponse.Code.Code, bisPushResponse.Description);
			}

			if (bisPushResponse != null && besPushResponse == null)
			{
				return new PushResult(pushId, bisPushResponse.Code.Code, bisPushResponse.Description);
			}
			if (bisPushResponse == null && besPushResponse != null)
			{
				return new PushResult(pushId, besPushResponse.Code.Code, besPushResponse.Description);
			}

			throw new ArgumentException("No push response received from the PPG(s). Unable to fulfill the push request");
		}

		private PushRequest BuildPushRequest(
			PushMessageControl bisPushMessageControl,
			PushMessageControl besPushMessageControl,
			int contentSize,
			PushResult pushResult,
			PushResponse bisPushResponse,
			PushResponse besPushResponse,
			IEnumerable<SubscriberPartial> activeBISSubscribers,
			IEnumerable<SubscriberPartial> activeBESSubscribers)
		{
			if ((bisPushMessageControl == null) && (besPushMessageControl == null))
			{
				throw new ArgumentNullException("Unable to create PushRequest since both control objects are null.");
			}

			PushMessageControl basePushMessageControl = null;
			if (bisPushMessageControl != null)
			{
				basePushMessageControl = bisPushMessageControl;
			}
			else
			{
				basePushMessageControl = besPushMessageControl;
			}

			PushRequest pushRequest =
				new PushRequest
				{
					Id = basePushMessageControl.PushId,
					PushApplicationId = basePushMessageControl.UserName,
					DeliveryMethod = basePushMessageControl.QualityOfService,
					ExpiryDate = basePushMessageControl.DeliverBefore,
				};
			if (bisPushMessageControl != null)
			{
				pushRequest.PublicNotifyUrl = bisPushMessageControl.PpgNotifyRequestTo;
			}
			if (besPushMessageControl != null)
			{
				pushRequest.EnterpriseNotifyUrl = besPushMessageControl.PpgNotifyRequestTo;
			}

			pushRequest.AddressCount = activeBISSubscribers.Count() + activeBESSubscribers.Count();

			List<SubscriberPartial> allActiveSubscribers = new List<SubscriberPartial>();
			allActiveSubscribers.AddRange(activeBISSubscribers);
			allActiveSubscribers.AddRange(activeBESSubscribers);
			pushRequest.Subscribers = allActiveSubscribers;

			pushRequest.ContentLength = contentSize;
			pushRequest.TotalContentLength = pushRequest.AddressCount * contentSize;

			if (bisPushResponse != null)
			{
				pushRequest.PublicStatusCode = bisPushResponse.Code;
				pushRequest.PublicStatusDescription = bisPushResponse.Description;
			}

			if (besPushResponse != null)
			{
				pushRequest.EnterpriseStatusCode = besPushResponse.Code;
				pushRequest.EnterpriseStatusDescription = besPushResponse.Description;
			}

			return pushRequest;
		}

		private SubscriptionValidationResult PerformSubscriptionValidation(
			PushParameters pushParameters, PushApplication pushApplication)
		{
			SubscriptionValidationResult subscriptionValidationResult;
			switch (pushApplication.Type)
			{
				case PushApplicationType.EnterprisePush:
					subscriptionValidationResult = PerformSubValidationBES(pushParameters, pushApplication);
					break;

				case PushApplicationType.PublicPush:
					subscriptionValidationResult = PerformSubValidationBIS(pushParameters, pushApplication);
					break;

				case PushApplicationType.PublicEnterprisePush:
					subscriptionValidationResult = PerformSubValidationBISBES(pushParameters, pushApplication);
					break;

				default:
					throw new ArgumentException(string.Format("Unknown push application type: {0}", pushApplication.Type));
			}

			//if (log.isDebugEnabled())
			//{
			//    log.debug(new StringBuilder().append("Number of doNotExistSubscriberIds: ").append(subscriptionValidationResult.getDoNotExistSubscriberIds().size()).toString());
			//    log.debug(new StringBuilder().append("Number of inactiveSubscriberIds: ").append(subscriptionValidationResult.getInactiveSubscriberIds().size()).toString());
			//    log.debug(new StringBuilder().append("Number of suspendedSubscriberIds: ").append(subscriptionValidationResult.getSuspendedSubscriberIds().size()).toString());
			//    log.debug(new StringBuilder().append("Number of activeAddresses: ").append(subscriptionValidationResult.getActiveAddresses().size()).toString());
			//}

			return subscriptionValidationResult;
		}

		private SubscriptionValidationResult PerformSubValidationBES(
			PushParameters pushParameters, PushApplication pushApplication)
		{
			SubscriptionValidationResult subscriptionValidationResult;

			if (pushParameters.Addresses.Any((item) => item.Equals("push_all")))
			{
				throw new ArgumentException("BES (enterprise) push does not allow for a push_all address.");
			}

			if (pushApplication.BypassSubscriptions)
			{
				subscriptionValidationResult = new SubscriptionValidationResult();
				foreach (string address in pushParameters.Addresses)
				{
					subscriptionValidationResult.ActiveAddresses.Add(address);
					SubscriberPartial subscriberPartial =
						new SubscriberPartial
						{
							PushApplicationId = pushParameters.ApplicationId,
							Address = address,
							Type = SubscriberType.Enterprise
						};
					subscriptionValidationResult.ActiveSubscribers.Add(subscriberPartial);
				}
			}
			else
			{
				subscriptionValidationResult = SubscriptionService.ValidateSubscriptions(
					pushParameters.ApplicationId, pushParameters.Addresses);
			}
			return subscriptionValidationResult;
		}

		private SubscriptionValidationResult PerformSubValidationBIS(
			PushParameters pushParameters, PushApplication pushApplication)
		{
			SubscriptionValidationResult subscriptionValidationResult = new SubscriptionValidationResult();
			if (pushParameters.Addresses.Any((item) => item.Equals("push_all")))
			{
				if (!pushApplication.IsStorePushRequests)
				{
					foreach (string address in pushParameters.Addresses)
					{
						subscriptionValidationResult.ActiveAddresses.Add(address);
						SubscriberPartial subscriberPartial =
							new SubscriberPartial
							{
								PushApplicationId = pushParameters.ApplicationId,
								Address = address,
								Type = SubscriberType.Public
							};
						subscriptionValidationResult.ActiveSubscribers.Add(subscriberPartial);
					}
				}
				else
				{
					int subscriberCount = SubscriptionService.GetSizeByAppIdAndStatus(pushApplication.RimApplicationId, SubscriberStatus.Active);
					int maxSubscriberCount = PushProperties.SubscriptionFindMaxResults;
					if (subscriberCount > maxSubscriberCount)
					{
						throw new ArgumentException(string.Format(
							"Number of addresses mapped to push_all is {0} which exceeds maximum of {1}.", subscriberCount, maxSubscriberCount));
					}

					var pushAllSubscribers = SubscriptionService.FindByAppIdAndStatus(
						pushApplication.RimApplicationId, SubscriberStatus.Active, 0, subscriberCount - 1);
					foreach (SubscriberPartial subscriber in pushAllSubscribers)
					{
						subscriptionValidationResult.ActiveAddresses.Add(subscriber.Address);
						subscriptionValidationResult.ActiveSubscribers.Add(subscriber);
					}
				}
			}
			else
			{
				// TODO: This is probably wrong - using addresses instead of subscriber ids.
				subscriptionValidationResult =
					SubscriptionService.ValidateSubscriptions(
					pushParameters.ApplicationId, pushParameters.Addresses);
			}
			return subscriptionValidationResult;
		}

		private SubscriptionValidationResult PerformSubValidationBISBES(
			PushParameters pushParameters, PushApplication pushApplication)
		{
			if (pushParameters.Addresses.Any((item) => item.Equals("push_all")))
			{
				throw new ArgumentException("BES (enterprise) push does not allow for a push_all address.");
			}

			SubscriptionValidationResult subscriptionValidationResult =
				SubscriptionService.ValidateSubscriptions(
					pushParameters.ApplicationId,
					pushParameters.Addresses);

			return subscriptionValidationResult;
		}

		private Task<StatusResult> StatusQuery(string applicationId, string pushId, string[] addresses, params MessageState[] states)
		{
			if (string.IsNullOrWhiteSpace(applicationId))
			{
				throw new ArgumentNullException("applicationId");
			}
			if (string.IsNullOrWhiteSpace(pushId))
			{
				throw new ArgumentNullException("pushId");
			}

			//if (log.isDebugEnabled())
			//{
			//    log.debug(new StringBuilder().append("Status query: appId=").append(applicationId).append(", pushId = ").append(pushId).append(", addresses=").append(addresses).append(", states=").append(states).toString());
			//}

			PushApplication pushApplication = ApplicationService.FindByAppId(applicationId);
			if (pushApplication == null)
			{
				//log.warn(new StringBuilder().append("Push Application could not be found: pushApplicationId=").append(applicationId).toString());
				throw new ArgumentException("Push application not found.");
			}

			if (pushApplication.ServiceLevel == PushApplicationServiceLevel.PushEssentials)
			{
				//log.warn("Status query is not available to Push Essentials applications.");
				throw new InvalidOperationException("Status query is not available to Push Essentials applications.");
			}

			return (pushApplication.IsStorePushRequests
				? GetLocalStatus(pushApplication, pushId, addresses, states)
				: GetRemoteStatus(pushApplication, pushId, addresses, states));
		}

		private Task<StatusResult> GetLocalStatus(
			PushApplication pushApplication,
			string pushId,
			string[] addresses,
			MessageState[] states)
		{
			PushRequest pushRequest = RequestService.FindById(pushId);
			if ((pushRequest == null) ||
				(!pushRequest.PushApplicationId.Equals(pushApplication.RimApplicationId, StringComparison.OrdinalIgnoreCase)))
			{
				//log.warn(new StringBuilder().append("Push Request does not exist: pushId=").append(pushId).toString());
				throw new ArgumentException("Push Request does not exist");
			}

			ISet<string> addressSet = new HashSet<string>();
			IEnumerable<PushRequestDetail> details;
			if ((addresses == null) || (addresses.Length == 0))
			{
				details = RequestDetailService.FindByIdAndStates(pushId, states);
			}
			else
			{
				if (pushApplication.BypassSubscriptions)
				{
					foreach (string address in addresses)
					{
						addressSet.Add(address.ToLower());
					}
					details = RequestDetailService.FindByIdAndAddresses(pushId, addresses);
				}
				else
				{
					foreach (string address in addresses)
					{
						addressSet.Add(address);
					}
					details = RequestDetailService.FindByIdAndSubscriberIds(pushId, addresses);
				}
			}

			StatusResult statusResult = new StatusResult(pushId, pushRequest.PushApplicationId);
			foreach (PushRequestDetail detail in details)
			{
				string address = pushApplication.BypassSubscriptions ? detail.Address : detail.SubscriberId;
				addressSet.Remove(address);
				statusResult.AddOutcome(
					address,
					detail.StatusCode,
					detail.StatusDescription,
					detail.CompletedDate,
					detail.MessageState,
					pushRequest.DeliveryMethod == DeliveryMethod.Confirmed);
			}

			DateTime eventTime = DateTime.UtcNow;
			foreach (String address in addressSet)
			{
				statusResult.AddOutcome(
					address,
					StatusCode.AddressNotFound,
					StatusCode.AddressNotFound.Description,
					eventTime,
					MessageState.Unknown,
					false);
			}

			TaskCompletionSource<StatusResult> tcs =
				new TaskCompletionSource<StatusResult>();
			tcs.SetResult(statusResult);
			return tcs.Task;
		}

		private async Task<StatusResult> GetRemoteStatus(
			PushApplication pushApplication,
			string pushId,
			string[] addresses,
			MessageState[] states)
		{
			ISet<string> doNotExistSubscriberIds = null;
			ISet<string> inactiveSubscriberIds = null;
			ISet<string> suspendedSubscriberIds = null;

			StatusResult statusResult = null;
			List<string> addressListBIS = null;
			List<string> addressListBES = null;
			if ((addresses != null) && (addresses.Any()) && (!AddressList.IsPushAll(addresses)))
			{
				if (pushApplication.BypassSubscriptions)
				{
					addressListBES = new List<string>();
					addressListBES.AddRange(addresses);
				}
				else
				{
					SubscriptionValidationResult subscriptionValidationResult =
						SubscriptionService.ValidateSubscriptions(
							pushApplication.RimApplicationId, addresses);

					doNotExistSubscriberIds = subscriptionValidationResult.DoNotExistSubscriberIds;
					inactiveSubscriberIds = subscriptionValidationResult.InactiveSubscriberIds;
					suspendedSubscriberIds = subscriptionValidationResult.SuspendedSubscriberIds;

					bool hasNoActiveAddresses = false;
					if (pushApplication.Type == PushApplicationType.EnterprisePush)
					{
						addressListBES = new List<string>();
						addressListBES.AddRange(subscriptionValidationResult.ActiveAddresses);
						hasNoActiveAddresses = !addressListBES.Any();
					}
					else if (pushApplication.Type == PushApplicationType.PublicPush)
					{
						addressListBIS = new List<string>();
						addressListBIS.AddRange(subscriptionValidationResult.ActiveAddresses);
						hasNoActiveAddresses = !addressListBIS.Any();
					}
					else
					{
						addressListBES = new List<string>();
						addressListBIS = new List<string>();

						foreach (SubscriberPartial activeSubs in subscriptionValidationResult.ActiveSubscribers)
						{
							if (activeSubs.Type == SubscriberType.Enterprise)
							{
								addressListBES.Add(activeSubs.Address);
							}
							else
							{
								addressListBIS.Add(activeSubs.Address);
							}
						}
						hasNoActiveAddresses = (!addressListBES.Any()) && (!addressListBIS.Any());
					}

					if (hasNoActiveAddresses)
					{
						statusResult = new StatusResult(
							pushId, pushApplication.RimApplicationId);
						statusResult.AddOutcome(
							null,
							StatusCode.AddressError,
							StatusCode.AddressError.Description,
							DateTime.UtcNow,
							MessageState.Unknown,
							false);
					}
				}
			}

			if (statusResult == null)
			{
				StatusQueryResponse response = null;
				try
				{
					if (pushApplication.Type == PushApplicationType.EnterprisePush)
					{
						StatusQueryMessageControl statusQueryMessage =
							new StatusQueryMessageControl(false);
						statusQueryMessage.PushId = pushId;
						statusQueryMessage.Addresses.AddRange(addressListBES);
						response = await PapService.StatusQuery(
							pushApplication.UserName,
							pushApplication.EnterprisePassword,
							statusQueryMessage);
					}
					else if (pushApplication.Type == PushApplicationType.PublicPush)
					{
						StatusQueryMessageControl statusQueryMessage =
							new StatusQueryMessageControl(true);
						statusQueryMessage.PushId = pushId;
						statusQueryMessage.Addresses.AddRange(addressListBIS);
						response = await PapService.StatusQuery(
							pushApplication.RimApplicationId,
							pushApplication.PublicPassword,
							statusQueryMessage);
					}
					else
					{
						if ((addresses == null) || (!addresses.Any()) || (addressListBES.Any()))
						{
							StatusQueryMessageControl statusQueryMessage =
								new StatusQueryMessageControl(false);
							statusQueryMessage.PushId = pushId;
							statusQueryMessage.Addresses.AddRange(addressListBES);
							response = await PapService.StatusQuery(
								pushApplication.UserName,
								pushApplication.EnterprisePassword,
								statusQueryMessage);
						}

						if ((addresses == null) || (!addresses.Any()) || (addressListBIS.Any()))
						{
							StatusQueryMessageControl statusQueryMessage =
								new StatusQueryMessageControl(true);
							statusQueryMessage.PushId = pushId;
							statusQueryMessage.Addresses.AddRange(addressListBIS);
							StatusQueryResponse statusQueryResponseBIS =
								await PapService.StatusQuery(
									pushApplication.RimApplicationId,
									pushApplication.PublicPassword,
									statusQueryMessage);

							if (statusQueryResponseBIS.Results.Count == 1)
							{
								StatusQueryResult result = (StatusQueryResult)
									statusQueryResponseBIS.Results.First();
								if ((!result.Addresses.Any()) &&
									(addressListBIS != null) &&
									(addressListBIS.Any()))
								{
									result.Addresses.AddRange(addressListBIS);
								}
								else if ((!result.Addresses.Any()) &&
									((addresses == null) || (!addresses.Any())) &&
									(response.Results.Any()))
								{
									statusQueryResponseBIS.Results.Clear();
								}
							}

							if (response == null)
							{
								response = statusQueryResponseBIS;
							}
							else
							{
								response.Results.AddRange(statusQueryResponseBIS.Results);
							}
						}
					}
				}
				catch (Exception e)
				{
					throw e;//new PushSDKException(e);
				}

				statusResult = new StatusResult(
					pushId, pushApplication.RimApplicationId);

				foreach (StatusQueryResult result in response.Results)
				{
					if ((StatusCode.PushIdNotFound.Equals(result.Code)) &&
						(pushApplication.Type != PushApplicationType.PublicEnterprisePush))
					{
						//log.warn(new StringBuilder().append("Push Request does not exist: pushId=").append(pushId).toString());
						throw new ArgumentException("Push Request does not exist");
					}

					if ((states == null) || (states.Contains(result.MessageState)))
					{
						bool applicationReliable = false;
						if (result.QualityOfService != null)
						{
							applicationReliable = (result.QualityOfService.DeliveryMethod == DeliveryMethod.Confirmed);
						}

						if (result.Addresses.Any())
						{
							foreach (string address in result.Addresses)
							{
								string subscriberId =
									GetSubscriberId(pushApplication, pushId, address);
								statusResult.AddOutcome(
									address,
									result.Code,
									result.Description,
									result.EventTime,
									result.MessageState,
									applicationReliable);
							}
						}
						else
						{
							statusResult.AddOutcome(
								null,
								result.Code,
								result.Description,
								result.EventTime,
								result.MessageState,
								applicationReliable);
						}
					}
				}
			}

			statusResult.DoNotExistSubscriberIds = doNotExistSubscriberIds;
			statusResult.InactiveSubscriberIds = inactiveSubscriberIds;
			statusResult.SuspendedSubscriberIds = suspendedSubscriberIds;
			return statusResult;
		}

		#region IPushService Members

		/// <summary>
		/// Sends a cancel request using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <returns></returns>
		public Task<CancelResult> Cancel(string applicationId, string pushId)
		{
			return Cancel(applicationId, pushId, null);
		}

		/// <summary>
		/// Sends a cancel request using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <param name="addresses">The addresses.</param>
		/// <returns></returns>
		public async Task<CancelResult> Cancel(string applicationId, string pushId, params string[] addresses)
		{
			if (string.IsNullOrWhiteSpace(applicationId))
			{
				throw new ArgumentNullException("applicationId");
			}
			if (string.IsNullOrWhiteSpace(pushId))
			{
				throw new ArgumentNullException("pushId");
			}

			//if (log.isDebugEnabled())
			//{
			//    log.debug(new StringBuilder().append("Cancel: appId=").append(applicationId).append(", pushId=").append(pushId).append(", addresses=").append(addresses).toString());
			//}

			PushApplication pushApplication = ApplicationService.FindByAppId(applicationId);
			if (pushApplication == null)
			{
				//log.warn(new StringBuilder().append("Push Application could not be found: pushApplicationId=").append(applicationId).toString());
				throw new ArgumentException("Push application not found.");
			}

			if (pushApplication.ServiceLevel == PushApplicationServiceLevel.PushEssentials)
			{
				//log.warn("Cancel is not available to Push Essentials applications.");
				throw new InvalidOperationException("Cancel is not available to Push Essentials applications.");
			}

			if (pushApplication.IsStorePushRequests)
			{
				PushRequest pushRequest = RequestService.FindById(pushId);
				if (pushRequest == null)
				{
					//log.warn(new StringBuilder().append("Push Request does not exist: pushId=").append(pushId).toString());
					throw new ArgumentException(string.Format("Push request does not exist: pushId={0}.", pushId));
				}
			}

			ISet<string> doNotExistSubscriberIds = null;
			ISet<string> inactiveSubscriberIds = null;
			ISet<string> suspendedSubscriberIds = null;

			CancelResult cancelResult = null;
			List<string> addressListBIS = null;
			List<string> addressListBES = null;
			if ((addresses != null) &&
				(addresses.Any()) &&
				(!AddressList.IsPushAll(addresses)))
			{
				if (pushApplication.BypassSubscriptions)
				{
					addressListBES = new List<string>();
					addressListBES.AddRange(addresses);
				}
				else
				{
					bool hasNoActiveAddresses = false;
					if (pushApplication.IsStorePushRequests)
					{
						var details = RequestDetailService.FindByIdAndSubscriberIds(pushId, addresses);
						if (pushApplication.Type == PushApplicationType.EnterprisePush)
						{
							addressListBES = new List<string>();
							foreach (PushRequestDetail detail in details)
							{
								addressListBES.Add(detail.Address);
							}

							hasNoActiveAddresses = !addressListBES.Any();
						}
						else if ((pushApplication.Type == PushApplicationType.PublicPush))
						{
							addressListBIS = new List<string>();
							foreach (PushRequestDetail detail in details)
							{
								addressListBIS.Add(detail.Address);
							}
							hasNoActiveAddresses = !addressListBIS.Any();
						}
						else
						{
							addressListBES = new List<string>();
							addressListBIS = new List<string>();
							foreach (PushRequestDetail detail in details)
							{
								if (detail.SubscriberType == SubscriberType.Public)
								{
									addressListBIS.Add(detail.Address);
								}
								else
								{
									addressListBES.Add(detail.Address);
								}
							}
							hasNoActiveAddresses =
								(!addressListBES.Any()) && (!addressListBIS.Any());
						}
					}
					else
					{
						SubscriptionValidationResult subscriptionValidationResult =
							SubscriptionService.ValidateSubscriptions(
								pushApplication.RimApplicationId, addresses);
						doNotExistSubscriberIds = subscriptionValidationResult.DoNotExistSubscriberIds;
						inactiveSubscriberIds = subscriptionValidationResult.InactiveSubscriberIds;
						suspendedSubscriberIds = subscriptionValidationResult.SuspendedSubscriberIds;

						if (pushApplication.Type == PushApplicationType.EnterprisePush)
						{
							addressListBES = new List<string>();
							addressListBES.AddRange(subscriptionValidationResult.ActiveAddresses);
							hasNoActiveAddresses = !addressListBES.Any();
						}
						else if (pushApplication.Type == PushApplicationType.PublicPush)
						{
							addressListBIS = new List<string>();
							addressListBIS.AddRange(subscriptionValidationResult.ActiveAddresses);
							hasNoActiveAddresses = !addressListBIS.Any();
						}
						else
						{
							addressListBES = new List<string>();
							addressListBIS = new List<string>();

							foreach (SubscriberPartial activeSubs in subscriptionValidationResult.ActiveSubscribers)
							{
								if (activeSubs.Type == SubscriberType.Enterprise)
								{
									addressListBES.Add(activeSubs.Address);
								}
								else
								{
									addressListBIS.Add(activeSubs.Address);
								}
							}
							hasNoActiveAddresses = (!addressListBES.Any()) && (!addressListBIS.Any());
						}
					}

					if (hasNoActiveAddresses)
					{
						cancelResult = new CancelResult(pushId);
						cancelResult.AddOutcome(null, StatusCode.AddressError);
					}
				}
			}

			if (cancelResult == null)
			{
				CancelResponse response = null;
				try
				{
					if (pushApplication.Type == PushApplicationType.EnterprisePush)
					{
						CancelMessageControl cancelMessage = new CancelMessageControl(false);
						cancelMessage.PushId = pushId;
						cancelMessage.Addresses.AddRange(addressListBES);
						response = await PapService.Cancel(
							pushApplication.UserName,
							pushApplication.EnterprisePassword,
							cancelMessage);
					}
					else if (pushApplication.Type == PushApplicationType.PublicPush)
					{
						CancelMessageControl cancelMessage = new CancelMessageControl(true);
						cancelMessage.PushId = pushId;
						cancelMessage.Addresses.AddRange(addressListBIS);
						response = await PapService.Cancel(
							pushApplication.RimApplicationId,
							pushApplication.PublicPassword,
							cancelMessage);
					}
					else
					{
						if ((addresses == null) || (!addresses.Any()) || (addressListBES.Any()))
						{
							CancelMessageControl cancelMessage = new CancelMessageControl(false);
							cancelMessage.PushId = pushId;
							cancelMessage.Addresses.AddRange(addressListBES);
							response = await PapService.Cancel(
								pushApplication.UserName,
								pushApplication.EnterprisePassword,
								cancelMessage);
						}

						if ((addresses == null) || (!addresses.Any()) || (addressListBIS.Any()))
						{
							CancelMessageControl cancelMessage = new CancelMessageControl(true);
							cancelMessage.PushId = pushId;
							cancelMessage.Addresses.AddRange(addressListBIS);
							CancelResponse cancelResponseBIS = await PapService.Cancel(
								pushApplication.RimApplicationId,
								pushApplication.PublicPassword,
								cancelMessage);

							if (response == null)
							{
								response = cancelResponseBIS;
							}
							else
							{
								response.Results.AddRange(cancelResponseBIS.Results);
							}
						}
					}
				}
				catch (/*BadMessage*/Exception e)
				{
					//throw new PushSDKException(e);
					throw e;
				}

				cancelResult = new CancelResult(pushId);
				foreach (var papCancelResult in response.Results)
				{
					StatusCode code = (papCancelResult.Code.Code.StartsWith("1")) ? StatusCode.Accepted : papCancelResult.Code;
					string description = (papCancelResult.Code.Code.StartsWith("1")) ? StatusCode.Accepted.Description : papCancelResult.Description;
					if (papCancelResult.Addresses.Any())
					{
						foreach (string address in papCancelResult.Addresses)
						{
							string subscriberId = GetSubscriberId(pushApplication, pushId, address);
							cancelResult.AddOutcome(subscriberId, code, description);
						}
					}
					else
					{
						cancelResult.AddOutcome(null, code, description);
					}
				}
			}

			cancelResult.DoNotExistSubscriberIds = doNotExistSubscriberIds;
			cancelResult.InactiveSubscriberIds = inactiveSubscriberIds;
			cancelResult.SuspendedSubscriberIds = suspendedSubscriberIds;
			return cancelResult;
		}

		/// <summary>
		/// Sends a push request using the attributes specified.
		/// </summary>
		/// <param name="pushParameters">The push parameters.</param>
		/// <returns></returns>
		public async Task<PushResult> Push(PushParameters pushParameters)
		{
			PushApplication pushApplication = ValidatePush(pushParameters);

			SubscriptionValidationResult subscriptionValidationResult =
				PerformSubscriptionValidation(pushParameters, pushApplication);

			PushResponse besPushResponse;
			PushResult pushResult;
			if (subscriptionValidationResult.ActiveAddresses.Any())
			{
				HashSet<SubscriberPartial> activeBISSubscribers = new HashSet<SubscriberPartial>();
				HashSet<SubscriberPartial> activeBESSubscribers = new HashSet<SubscriberPartial>();
				HashSet<string> activeBISAddresses = new HashSet<string>();
				HashSet<string> activeBESAddresses = new HashSet<string>();

				foreach (SubscriberPartial subscriberPartial in subscriptionValidationResult.ActiveSubscribers)
				{
					if (subscriberPartial.Type == SubscriberType.Enterprise)
					{
						activeBESAddresses.Add(subscriberPartial.Address);
						activeBESSubscribers.Add(subscriberPartial);
					}
					else
					{
						activeBISAddresses.Add(subscriberPartial.Address);
						activeBISSubscribers.Add(subscriberPartial);
					}
				}

				int totalActiveAddress = activeBISAddresses.Count + activeBESAddresses.Count;

				string pushId = ValidatePushId(
					pushParameters.PushId,
					pushApplication.IsStorePushRequests);
				pushParameters.PushId = pushId;

				PushResponse bisPushResponse = null;
				PushMessageControl bisPushMessageControl = null;
				if (activeBISAddresses.Any())
				{
					bisPushMessageControl = CreatePushMessageControl(
						true,
						pushId,
						pushParameters,
						pushApplication,
						activeBISAddresses,
						totalActiveAddress);
					//try
					{
						bisPushResponse = await PapService.Push(
							pushParameters.ApplicationId,
							pushApplication.PublicPassword,
							pushParameters.ApplicationId,
							bisPushMessageControl,
							pushParameters.Content);
					}
					//catch (BadMessageException e)
					//{
					//    log.error("BadMessageException caught:", e);
					//    throw new PushSDKException(e);
					//}
				}

				besPushResponse = null;
				PushMessageControl besPushMessageControl = null;
				if (activeBESAddresses.Any())
				{
					besPushMessageControl = CreatePushMessageControl(
						false,
						pushId,
						pushParameters,
						pushApplication,
						activeBESAddresses,
						totalActiveAddress);
					//try
					{
						besPushResponse = await PapService.Push(
							pushApplication.UserName,
							pushApplication.EnterprisePassword,
							pushApplication.PushPort.ToString(),
							besPushMessageControl,
							pushParameters.Content);
					}
					//catch (BadMessageException e)
					//{
					//    log.error("BadMessageException caught:", e);
					//    throw new PushSDKException(e);
					//}
				}

				int contentSize = CalculateContentLength(pushParameters.Content);

				pushResult = CreatePushResult(pushId, bisPushResponse, besPushResponse);
				if (pushApplication.IsStorePushRequests)
				{
					PushRequest pushRequest = BuildPushRequest(
						bisPushMessageControl,
						besPushMessageControl,
						contentSize,
						pushResult,
						bisPushResponse,
						besPushResponse,
						activeBISSubscribers,
						activeBESSubscribers);
					RequestService.AddPushRequest(pushRequest);

					if (pushApplication.IsTrackingLastNotification &&
						pushRequest.AddressCount > 1)
					{
						CountService.Add(pushId);
					}
				}
				else if (bisPushMessageControl != null &&
					bisPushMessageControl.Addresses.Any((item) => item.Equals("push_all", StringComparison.OrdinalIgnoreCase)))
				{
					totalActiveAddress = SubscriptionService.GetSizeByAppIdAndStatus(
						pushApplication.RimApplicationId, SubscriberStatus.Active);
				}

				bool isBisPushSuccessful = (bisPushResponse != null) && (bisPushResponse.Code.Code.StartsWith("1"));
				bool isBesPushSuccessful = (besPushResponse != null) && (besPushResponse.Code.Code.StartsWith("1"));

				if ((isBisPushSuccessful) || (isBesPushSuccessful))
				{
					StatsService.IncrementPushStats(
						pushApplication.RimApplicationId,
						totalActiveAddress,
						totalActiveAddress * contentSize);
				}

				if ((!isBisPushSuccessful) && (bisPushResponse != null))
				{
					//log.error(new StringBuilder().append("The public/BIS push failed. pushId=").append(pushId).append(", code=").append(bisPushResponse.getCode()).append(", desc=").append(bisPushResponse.getDescription()).toString());
					if (AddressList.IsPushAll(pushParameters.Addresses))
					{
						FailedPushResult failedPushResult =
							new FailedPushResult(
								AddressList.PushAll,
								bisPushResponse.Code,
								bisPushResponse.Description);
						pushResult.FailedPushResults.Add(failedPushResult);
					}
					else
					{
						foreach (SubscriberPartial activeBISSub in activeBISSubscribers)
						{
							FailedPushResult failedPushResult =
								new FailedPushResult(
									activeBISSub.SubscriberId,
									bisPushResponse.Code,
									bisPushResponse.Description);
							pushResult.FailedPushResults.Add(failedPushResult);
						}
					}
				}

				if ((!isBesPushSuccessful) && (besPushResponse != null))
				{
					//log.error(new StringBuilder().append("The enterprise/BES push failed. pushId=").append(pushId).append(", code=").append(besPushResponse.getCode()).append(", desc=").append(besPushResponse.getDescription()).toString());
					foreach (SubscriberPartial activeBESSub in activeBESSubscribers)
					{
						FailedPushResult failedPushResult =
							new FailedPushResult(
								activeBESSub.SubscriberId,
								besPushResponse.Code,
								besPushResponse.Description);
						pushResult.FailedPushResults.Add(failedPushResult);
					}
				}
			}
			else
			{
				//log.warn("No active addresses to push to; push will not be performed.");
				pushResult = new PushResult(
					pushParameters.PushId, StatusCode.AddressError);
			}

			pushResult.PushId = pushParameters.PushId;
			pushResult.RemainingQuota = StatsService.GetRemainingQuota(
				pushApplication.RimApplicationId, true);
			pushResult.DoNotExistSubscriberIds =
				subscriptionValidationResult.DoNotExistSubscriberIds;
			pushResult.InactiveSubscriberIds =
				subscriptionValidationResult.InactiveSubscriberIds;
			pushResult.SuspendedSubscriberIds =
				subscriptionValidationResult.SuspendedSubscriberIds;
			return pushResult;
		}

		/// <summary>
		/// Sends a status query using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <returns></returns>
		public Task<StatusResult> StatusQueryAll(string applicationId, string pushId)
		{
			return StatusQuery(applicationId, pushId, null, null);
		}

		/// <summary>
		/// Sends a status query using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <param name="addresses">The addresses.</param>
		/// <returns></returns>
		public Task<StatusResult> StatusQueryAll(string applicationId, string pushId, params string[] addresses)
		{
			return StatusQuery(applicationId, pushId, addresses, null);
		}

		/// <summary>
		/// Sends a status query using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <returns></returns>
		public Task<StatusResult> StatusQueryCompleted(string applicationId, string pushId)
		{
			return StatusQuery(applicationId, pushId, null,
				MessageState.Delivered);
		}

		/// <summary>
		/// Sends a status query using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <returns></returns>
		public Task<StatusResult> StatusQueryFailed(string applicationId, string pushId)
		{
			return StatusQuery(applicationId, pushId, null,
				MessageState.Undeliverable,
				MessageState.Rejected,
				MessageState.Aborted,
				MessageState.Expired,
				MessageState.Timeout);
		}

		/// <summary>
		/// Sends a status query using the attributes specified.
		/// </summary>
		/// <param name="applicationId">The application id.</param>
		/// <param name="pushId">The push id.</param>
		/// <returns></returns>
		public Task<StatusResult> StatusQueryPending(string applicationId, string pushId)
		{
			return StatusQuery(applicationId, pushId, null,
				MessageState.Pending);
		}

		#endregion
	}
}
